package com.example.service.impl;

import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.config.RabbitMQConfig;
import com.example.constant.RedisKey;
import com.example.controller.request.TrafficPageRequest;
import com.example.controller.request.UseTrafficRequest;
import com.example.enums.BizCodeEnum;
import com.example.enums.EventMessageType;
import com.example.enums.TaskStateEnum;
import com.example.exception.BizException;
import com.example.feign.ProductFeignService;
import com.example.feign.ShortLinkFeignService;
import com.example.interceptor.LoginInterceptor;
import com.example.manager.TrafficManager;
import com.example.manager.TrafficTaskManager;
import com.example.model.EventMessage;
import com.example.model.TrafficDO;
import com.example.model.TrafficTaskDO;
import com.example.service.TrafficService;
import com.example.utils.JsonData;
import com.example.utils.JsonUtil;
import com.example.utils.TimeUtil;
import com.example.vo.ProductVO;
import com.example.vo.TrafficVO;
import com.example.vo.UsedTrafficVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author yinxin
 * @version 1.0
 * @Description:
 * @date 2022/6/2214:46
 */
@Service
@Slf4j
public class TrafficServiceImpl implements TrafficService {

    @Autowired
    private TrafficManager trafficManager;

    @Autowired
    private ProductFeignService productFeignService;

    @Autowired
    RedisTemplate<Object,Object> redisTemplate;

    @Autowired
    TrafficTaskManager trafficTaskManager;

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Autowired
    RabbitMQConfig rabbitMQConfig;

    @Autowired
    ShortLinkFeignService shortLinkFeignService;

    /**
     * 新增流量包 消费商品模块的下单支付的成功后的回调消息
     * @param eventMessage
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public Boolean handleTrafficMessage(EventMessage eventMessage) {
        Long accountNo = eventMessage.getAccountNo();
        String messageType = eventMessage.getEventMessageType();
        if(EventMessageType.PRODUCT_ORDER_PAY.name().equalsIgnoreCase(messageType)){
            //订单已经支付，新增流量包
            String content = eventMessage.getContent();
            Map<String,Object> orderInfoMap = JsonUtil.json2Obj(content, Map.class);

            String outTradeNo = (String) orderInfoMap.get("outTradeNo");
            Integer buyNum = (Integer)orderInfoMap.get("buyNum");
            String productStr = (String) orderInfoMap.get("product");
            ProductVO productVO = JsonUtil.json2Obj(productStr, ProductVO.class);
            log.info("商品信息:{}",productVO);

            //流量包有效期
            LocalDateTime expireDateTime = LocalDateTime.now().plusDays(productVO.getValidDay());
            Date date = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());

            //构建流量包对象
            TrafficDO trafficDO = TrafficDO.builder()
                    .accountNo(accountNo)
                    .dayLimit(productVO.getDayTimes() * buyNum)
                    .dayUsed(0)
                    .totalLimit(productVO.getTotalTimes())
                    .pluginType(productVO.getPluginType())
                    .level(productVO.getLevel())
                    .productId(productVO.getId())
                    .outTradeNo(outTradeNo)
                    .expiredDate(date)
                    .build();
            int rows = trafficManager.add(trafficDO);
            log.info("消费消息新增流量包:{},rows:{}",trafficDO,rows);

            //新增流量包删除key
            String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
            redisTemplate.delete(totalTrafficTimesKey);

        }else if(EventMessageType.TRAFFIC_FREE_INIT.name().equalsIgnoreCase(messageType)){
            //发放免费流量包
            Long productId = Long.valueOf(eventMessage.getBizId());
            ProductVO productVO = productFeignService.findDetailById(productId)
                    .getData(new TypeReference<ProductVO>(){});
            TrafficDO trafficDO = TrafficDO.builder()
                    .accountNo(accountNo)
                    .dayUsed(0)
                    .totalLimit(productVO.getTotalTimes())
                    .pluginType(productVO.getPluginType())
                    .level(productVO.getLevel())
                    .productId(productVO.getId())
                    .outTradeNo("free_init")
                    .expiredDate(new Date())
                    .build();
            trafficManager.add(trafficDO);
        }else if(EventMessageType.TRAFFIC_USED.name().equalsIgnoreCase(messageType)){
            //流量包使用，检查是否成功使用
            //检查task是否存在
            //检查是否是成功
            //如果不成功要恢复流量包
            //删除task（或者跟新task状态，定时删除）
            Long trafficTaskId = Long.valueOf(eventMessage.getBizId());
            TrafficTaskDO trafficTaskDO = trafficTaskManager.findByIdAndAccountNo(trafficTaskId, accountNo);
            if(trafficTaskDO!=null && trafficTaskDO.getLockState().equalsIgnoreCase(TaskStateEnum.LOCK.name())){
                //检查task是否存在
                JsonData jsonData = shortLinkFeignService.check(trafficTaskDO.getBizId());
                if(jsonData.getCode()!=200){
                    log.error("创建短链失败，流量包回滚");
                    String useDateStr = TimeUtil.format(trafficTaskDO.getGmtCreate(), "yyyy-MM-dd");
                    //回滚流量包(当跨日期减少用的流量包时：昨天晚上11:59创建失败，次日恢复了使用流量包清零，然后正好这个延迟队列的消息还是消费就拒绝消费)
                    //这里用useDateStr控制
                    trafficManager.releaseUsedTimes(accountNo,trafficTaskDO.getTrafficId(),1,useDateStr);
                    //恢复流量包删除key
                    String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
                    redisTemplate.delete(totalTrafficTimesKey);
                }
                //多种方式处理task，不用立刻删除，可以更新状态，然后定时删除也行 eg:跟新task状态然后定时任务删除
                trafficTaskManager.deleteByIdAndAccountNo(trafficTaskId,accountNo);

            }
        }
        return true;
    }


    /**
     * 分页查询商品信息
     * @param request
     * @return
     */
    @Override
    public Map<String, Object> pageAvailable(TrafficPageRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        IPage<TrafficDO> trafficDOIPage = trafficManager.pageAvailable(request.getPage(), request.getSize(), accountNo);
        HashMap<String, Object> result = new HashMap<>();
        List<TrafficDO> records = trafficDOIPage.getRecords();
        result.put("total_record",trafficDOIPage.getTotal());
        result.put("total_page",trafficDOIPage.getPages());
        result.put("current_data",records.stream().map(o -> beanProcess(o)).collect(Collectors.toList()));
        return result;
    }

    /**
     * 查询商品详情
     * @param trafficId
     * @return
     */
    @Override
    public TrafficVO detail(Long trafficId) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        TrafficDO trafficDO = trafficManager.findByIdAndAccountNo(trafficId, accountNo);
        return beanProcess(trafficDO);
    }

    /**
     * 删除过期流量包
     * @return
     */
    @Override
    public boolean deleteExpireTraffic() {
        return trafficManager.deleteExpireTraffic();
    }


    /**
     *  一、扣减短链包流程
     *  1、找出剩余次数和当前使用的短链,和过期但今天未跟新的短链
     *  2、当存在未跟新的短链就更新这些短链将使用次数重置为0
     *  3、增加扣减短链使用次数（mysql）lock状态并存储备份数据用于base事务的保证
     *  4、将这个扣减的短链存一份在task表中用于核验再扣减数据库使用次序后这个流量包是否正确的插入数据库
     *  5、数据库增加流量包使用次数，并将真实数据跟新到redis。
     *  6、使用延迟队列去根据task存储的数据去检查短链包是否插入成功。
     */
    @Override
    @Transactional(rollbackFor = Exception.class,propagation = Propagation.REQUIRED)
    public JsonData reduce(UseTrafficRequest trafficRequest) {
        Long accountNo = trafficRequest.getAccountNo();
        //找出剩余次数，当前用的短链包和未使用的包。
        UsedTrafficVO usedTrafficVO = processTrafficList(accountNo);
        log.info("天剩余可用次数:{};当前使用流量包:{},没有过期的流量包:{}",usedTrafficVO.getDayTotalLeftTimes()
                ,usedTrafficVO.getCurrentTrafficDO(),usedTrafficVO.getUnUpdateTrafficIds());
        if(usedTrafficVO.getCurrentTrafficDO()==null){
            return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }
        log.info("待跟新流量包列表{}",usedTrafficVO.getUnUpdateTrafficIds());
        if(usedTrafficVO.getUnUpdateTrafficIds().size()>0){
            //跟新今日流量包
            trafficManager.batchUpdateUsedTimes(accountNo,usedTrafficVO.getUnUpdateTrafficIds());
        }
        //先更新再扣减当前使用的流量包
        int rows = trafficManager.addDayUsedTimes(accountNo, usedTrafficVO.getCurrentTrafficDO().getId(), 1);
        //TrafficTaskDO用于核验再扣减数据库使用次序后这个流量包是否正确的插入数据库
        TrafficTaskDO trafficTaskDO = TrafficTaskDO.builder().accountNo(accountNo)
                .bizId(trafficRequest.getBizId()).useTimes(1).trafficId(usedTrafficVO.getCurrentTrafficDO().getId())
                .lockState(TaskStateEnum.LOCK.name()).build();

        trafficTaskManager.add(trafficTaskDO);


        if(rows != 1){
            throw new BizException(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }
        //往redis设置下总流量包次数,短链包那边递减即可;如果有新增流量包,则删除这个key
        Integer remainSecondsOneDay = TimeUtil.getRemainSecondsOneDay(new Date());
        String totalTrafficTimesKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC, accountNo);
        redisTemplate.opsForValue().set(totalTrafficTimesKey,
                usedTrafficVO.getDayTotalLeftTimes()-1,remainSecondsOneDay, TimeUnit.SECONDS);

        //事务消息利用延迟队列去核对最后数据库中的是否有创建的短链
        EventMessage trafficEventMessage = EventMessage.builder().accountNo(accountNo)
                .bizId(trafficTaskDO.getId()+"")
                .eventMessageType(EventMessageType.TRAFFIC_USED.name())
                .build();
        //延迟消息，用于异常回滚
        rabbitTemplate.convertAndSend(rabbitMQConfig.getTrafficEventExchange()
                ,rabbitMQConfig.getTrafficReleaseDelayRoutingKey(),trafficEventMessage);

        return JsonData.buildSuccess();
    }

    /**
     * - 查询用户全部可用流量包
     * - 遍历用户可用流量包
     *   - 判断是否更新-用日期判断（要么都更新过，要么都没更新，根据gmt_modified）
     *     - 没更新的流量包后加入【待更新集合】中
     *       - 增加【今天剩余可用总次数】
     *     - 已经更新的判断是否超过当天使用次数
     *       - 如果没超过则增加【今天剩余可用总次数】
     *       - 超过则忽略
     * - 更新用户今日流量包相关数据
     * - 扣减使用的某个流量包使用次数
     * @return
     */
    private UsedTrafficVO processTrafficList(Long accountNo) {
        //全部流量包
        List<TrafficDO> trafficDOS = trafficManager.selectAvailableTraffics(accountNo);
        if(trafficDOS==null||trafficDOS.size()==0){
            throw new BizException(BizCodeEnum.TRAFFIC_EXCEPTION);
        }
        //天剩余可用总次数
        Integer dayTotalLeftTimes = 0;
        //当前使用
        TrafficDO currentTrafficDO = null;
        //没过期，但是今天没更新的流量包id列表
        List<Long> unUpdateTrafficIds = new ArrayList<>();
        String today = TimeUtil.format(new Date(), "yyyy-MM-dd");
        for (TrafficDO trafficDO : trafficDOS) {
            //过期会被定时任务删除
            String trafficUpdateDate = TimeUtil.format(trafficDO.getGmtModified(), "yyyy-MM-dd");
            if(today.equalsIgnoreCase(trafficUpdateDate)){
                //已经更新
                int dayLeftTimes = trafficDO.getDayLimit() - trafficDO.getDayUsed();
                dayTotalLeftTimes = dayTotalLeftTimes + dayLeftTimes;
                //选取当次使用
                if(dayLeftTimes>0 && currentTrafficDO==null){
                    currentTrafficDO = trafficDO;
                }
            }else {
                //未更新
                dayTotalLeftTimes = dayTotalLeftTimes + trafficDO.getDayLimit();
                //记录未跟新的流量包
                unUpdateTrafficIds.add(trafficDO.getId());
                //选取当次使用流量包
                if(currentTrafficDO == null){
                    currentTrafficDO = trafficDO;
                }
            }
        }
        return new UsedTrafficVO(dayTotalLeftTimes,currentTrafficDO,unUpdateTrafficIds);
    }

    //拷贝bean
    private TrafficVO beanProcess(TrafficDO traffic) {
        TrafficVO trafficVO = new TrafficVO();
        BeanUtils.copyProperties(traffic,trafficVO);
        return trafficVO;
    }
}
